{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../common/rfsoc_book_banner.jpg\" alt=\"University of Strathclyde\" align=\"left\">"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-block\" style=\"background-color: #c7b8d6; padding: 10px\">\n",
    "    <p style=\"color: #222222\">\n",
    "        <b>Note:</b>\n",
    "        <br>\n",
    "        This Jupyter notebook uses hardware features of the Zynq UltraScale+ RFSoC device. Therefore, the notebook cells will only execute successfully on an RFSoC platform.\n",
    "    </p>\n",
    "</div>\n",
    "\n",
    "# Notebook Set G\n",
    "\n",
    "---\n",
    "\n",
    "## 03 - RFSoC Radio Hello World!\n",
    "The final notebook in this set will use the RFSoC radio system to send messages from the transmitter to the receiver. The first message we will send is 'Hello World!'. Then, we will use a timer thread class to send a repeating message. A text terminal widget will be used to inspect received messages from the transmitter. Lastly, an image will be sent from the transmitter to the receiver. Received images will be displayed in the Jupyter notebook.\n",
    "\n",
    "## Table of Contents\n",
    "* [1. Introduction](#introduction)\n",
    "* [2. Receiving Messages](#receiving-messages)\n",
    "* [3. Hello World!](#hello-world)\n",
    "* [4. A Repeating Message](#a-repeating-message)\n",
    "* [5. Image Transfer](#image-transfer)\n",
    "* [6. Conclusion](#conclusion)\n",
    "\n",
    "## References\n",
    "* [1] - [Xilinx, Inc, \"USP RF Data Converter: LogiCORE IP Product Guide\", PG269, v2.4, November 2020](https://www.xilinx.com/support/documentation/ip_documentation/usp_rf_data_converter/v2_4/pg269-rf-data-converter.pdf)\n",
    "* [2] - [Stewart, R. W., Barlee, K. W., Atkinson, D. S. W., & Crockett, L. H. (2015). Software Defined Radio using MATLAB & Simulink and the RTL-SDR. (1 ed.)](https://www.desktopsdr.com/)\n",
    "\n",
    "## Revision\n",
    "* **v1.0** | 13/01/23 | *First Revision*\n",
    "\n",
    "---\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Introduction\n",
    "Let us begin by programming the FPGA bitstream and initialising the PYNQ overlay design. To do this, we need to import the `rfsoc_radio` package.\n",
    "\n",
    "Upon executing the code cell below, two tests will run to ensure the radio system is operational. If these tests fail, check your platform's loopback connection, restart the kernel, and try running the code cell again.\n",
    "\n",
    "<div class=\"alert alert-box alert-danger\">\n",
    "<b>Caution:</b>\n",
    "    In this demonstration, we generate signals using the RFSoC development board. Your device should be setup in loopback mode. You should understand that the RFSoC platform can also transmit RF signals wirelessly. Remember that unlicensed wireless transmission of RF signals may be illegal in your geographical location. Radio signals may also interfere with nearby devices, such as pacemakers and emergency radio equipment. Note that it is also illegal to intercept and decode particular RF signals. If you are unsure, please seek professional support.\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from rfsoc_radio.overlay import RadioOverlay\n",
    "\n",
    "ol = RadioOverlay(run_test=True)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the previous notebook, we launched a dashboard to control the radio system. We can launch this again by running the following code cell."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.dashboard()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* _**After you have executed the above cell, you should right click the radio dashboard, and select \"Create New View for Output\" from the drop-down menu. This will allow you to interact with the notebook and retain access to the radio dashboard in another output view.**_"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Receiving Messages\n",
    "This RFSoC demonstration system has been designed to simplify data movement between the RFSoC's PS and PL. It is worth noting that the transmitter and receiver systems are entirely independant of one another. That is, they do not share common clocks and there are no hidden loopbacks in the logic fabric. To transmit and receive using the radio demonstrator, we are relying entirely on the RF DC interface.\n",
    "\n",
    "Ascii terminals have been created using `ipywidgets` that allow you to visualise and interact with transmitted and received data. Lets start by creating the receiver terminal by running the code cell below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.radio_receiver.terminal()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The receiver terminal will appear. You won't be able to interact with the main text box as this terminal is for receiving messages only. That means we need to transmit a message first before it will appear in the box above. You can configure the receiver terminal using the buttons on the right. The functionality of each button is as follows:\n",
    "\n",
    "* **Play button** - Listen for transmitted BPSK and QPSK waveforms with the extended barker sequence and print them in the terminal.\n",
    "* **Stop button** - Do not listen for transmitted BPSK and QPSK waveforms with the extended barker sequence.\n",
    "* **Clear button** - Clear the terminal.\n",
    "* **Auto Clear button** - Automatically clear the terminal after 10 messages have been received.\n",
    "* **Debug button** - When enabled, inspect the frame's meta data and payload information. When disabled, only show the payload.\n",
    "\n",
    "Lets now put our receiver terminal to good use by transmitting a QPSK signal containing 'Hello World!'.\n",
    "\n",
    "* _**Right-click the receiver terminal above, and in the drop-down menu that appears, select \"Create New View for Output\". This action will move the terminal to another window in Jupyter Labs, allowing you to scroll further down the notebook while still being able to visualise and interact with the plots.**_"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Hello World!\n",
    "Run the code cell below to initiate a data transfer between the RF DAC and RF ADC."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.radio_transmitter.data('Hello World!\\r')\n",
    "ol.radio_transmitter.start()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now that you have transmitted and received your first 'Hello World!' message, lets transmit another message. This time, ensure that the **Debug Button** on the receiver terminal is enabled (blue colour). You can see this button enabled in Figure 1 below. This function will allow you to inspect the frame information when you run the next code cell.\n",
    "\n",
    "<figure> <a class=\"anchor\" id=\"fig-1\"></a>\n",
    "    <img src=\"images/receiver_terminal_debug.jpg\" style=\"width: 30%;\"/>\n",
    "    <figcaption><b>Figure 1: Switch on the Debug button.</b></figcaption>\n",
    "</figure>\n",
    "\n",
    "Now run the next code cell, which will transmit several QPSK frames rather than just one."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.radio_transmitter.data(''.join(['The quick brown fox jumps over the lazy dog.\\r',\n",
    "                             'How razorback-jumping frogs can level six piqued gymnasts.\\r']))\n",
    "ol.radio_transmitter.start()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the receiver terminal, you will now be able to see information about the packets that were just sent. A total of 3 packets will have been received. You can use this debug feature to inspect the frame meta data.\n",
    "\n",
    "* _**From this point on you should switch off the debug feature using the debug button and clear your receiver terminal using the clear button.**_\n",
    "\n",
    "Another terminal can be created that will allow you to insert ascii data directly into the transmitter. Running the code cell below, will create a transmitter terminal."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.radio_transmitter.terminal()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To use this terminal, all you have to do is insert a message and press the send button. After you press the send button, the terminal will automatically clear ready for the next input. Try this now and use the receiver terminal to inspect the received data."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. A Repeating Message\n",
    "The next part of the demonstration is a repeating message. We will introduce a simple timer thread class, which will allow you to execute a function at a specified rate and number of iterations. Run the code cell below to create the timer thread class."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import threading\n",
    "import time\n",
    "\n",
    "class TimerThread():\n",
    "    def __init__(self,\n",
    "                 callback,\n",
    "                 rate=0.5,\n",
    "                 iterations=20):\n",
    "    \n",
    "        self.callback = callback\n",
    "        self.rate = rate\n",
    "        self.iterations = iterations\n",
    "        self.stopped = True\n",
    "    \n",
    "    def start(self):\n",
    "        if self.stopped:\n",
    "            thread = threading.Thread(target=self._do)\n",
    "            thread.start()\n",
    "            \n",
    "    def _do(self):\n",
    "        self.stopped = False\n",
    "        iterations = 0\n",
    "        while iterations < self.iterations:\n",
    "            next_timer = time.time() + self.rate\n",
    "            self.callback()\n",
    "            iterations += 1\n",
    "            sleep_time = next_timer - time.time()\n",
    "            if sleep_time > 0:\n",
    "                time.sleep(sleep_time)\n",
    "            if self.stopped:\n",
    "                break\n",
    "        self.stopped = True\n",
    "        \n",
    "    def stop(self):\n",
    "        self.stopped = True"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The timer thread class accepts three arguments, a callback, the execution rate of the callback function, and the number of times the callback should be executed. We can create a callback function for the repeating message as below in `transmitter_callback()`. This function uses a global counter, to create a 'Hello World!' message with a number appended at the end. The callback will be passed to the timer thread class for execution.\n",
    "\n",
    "Run the code cell below to create the TimerThread object and transmitter callback."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "counter = 0\n",
    "\n",
    "def transmitter_callback():\n",
    "    global counter\n",
    "    message = ''.join(['Hello World! ', str(counter), '\\r'])\n",
    "    ol.radio_transmitter.data(message)\n",
    "    ol.radio_transmitter.start()\n",
    "    counter += 1\n",
    "\n",
    "tx_repeater = TimerThread(callback=transmitter_callback,\n",
    "                          rate=0.5,\n",
    "                          iterations=20)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can now start the thread by running the code cell below. Doing so will create a new thread that will execute the `transmitter_callback()` function 20 times, every 0.5 seconds. After the number of iterations have been achieved, the thread will exit. Before running the code cell below, ensure you can see your receiver terminal."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tx_repeater.start()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If at any point you wish to stop the thread, simply use the cell below. Remember the thread will stop anyway when the number of iterations have been achieved."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tx_repeater.stop()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Image Transfer\n",
    "The final part of this demonstration is to transfer an image between the transmitter and receiver. We can send an image by converting it to bytes and transmitting the data for the receiver to acquire.\n",
    "\n",
    "The code cell below performs many different tasks. Primarily, the image is prepared for transmission and displayed to the user. After each image transmission, the original image will be swapped for another that has been rotated by 90°. Rotating the image will allow us to see when a new image has been acquired at the receiver."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Import ImageViewer() from the quick widgets library\n",
    "from rfsoc_radio.quick_widgets import ImageViewer\n",
    "import numpy as np\n",
    "import ipywidgets as ipw\n",
    "\n",
    "# Open the target image using bytes\n",
    "image = []\n",
    "for i in range(4):\n",
    "    file = open('images/small_cat_grey_' + str(i) + '.jpg', \"rb\")\n",
    "    image.append(file.read())\n",
    "\n",
    "# Set flip variable\n",
    "counter = 0\n",
    "\n",
    "# Create image viewer for transmitted image\n",
    "sendimage = ImageViewer(description='Image to Transmit')\n",
    "\n",
    "def sendimage_callback():\n",
    "    global counter\n",
    "    global image\n",
    "    ol.radio_transmitter.data(image[counter])\n",
    "    sendimage.update(image[counter])\n",
    "    if counter > 2:\n",
    "        counter = 0\n",
    "    else:\n",
    "        counter += 1\n",
    "    \n",
    "# Set the sendimage_callback function as the callback before transmitting\n",
    "# data using the transmitter\n",
    "ol.radio_transmitter.monitor.callback = [sendimage_callback]\n",
    "\n",
    "# Slow down the transmission rate to two seconds\n",
    "ol.radio_transmitter.monitor.rate = 2\n",
    "\n",
    "sendimage.update(image[0])\n",
    "\n",
    "# Get the widget for interaction with sent data\n",
    "ipw.HBox([sendimage.get_widget()])"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The image above has a resolution of 50x50 pixels and is greyscale. The code cell below will prepare the receiver for acquiring this image and displaying it to the user. Upon executing this code cell, you will see a broken image icon. Don't worry about this! As soon as an image is received, this icon will dissapear and the newly received image will take its place."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Import ImageViewer() from the quick widgets library\n",
    "from rfsoc_radio.quick_widgets import ImageViewer\n",
    "import numpy as np\n",
    "import ipywidgets as ipw\n",
    "\n",
    "# Create a receiver buffer to store packets of data\n",
    "recvbuffer = np.empty(0, dtype=np.uint8)\n",
    "\n",
    "# Create an image viewer object for visualising the received data\n",
    "recvimage = ImageViewer(description='Received Image')\n",
    "\n",
    "# Create a custom callback function that is executed when the receiver\n",
    "# interrupt is triggered\n",
    "def recvimage_callback():\n",
    "    global recvbuffer\n",
    "    frame = ol.radio_receiver.frame\n",
    "    payload = frame[\"payload\"]\n",
    "    if ((frame[\"flags\"] >> 1) & 1):\n",
    "        recvbuffer = np.array(payload, dtype=np.uint8)\n",
    "    else:\n",
    "        recvbuffer = np.append(recvbuffer, payload)\n",
    "    if (frame[\"flags\"] & 1):\n",
    "        recvimage.update(recvbuffer.tobytes())\n",
    "        \n",
    "# Set the terminal_callback function as the callback for the receiver\n",
    "# when an interrupt is triggered\n",
    "ol.radio_receiver.monitor.callback = [recvimage_callback]\n",
    "\n",
    "# Get the widget for interaction with received data\n",
    "ipw.HBox([recvimage.get_widget()])"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The transmitter can be configured to run a callback after successfully sending data. To enable this functionality, set the transmitter to repeat and start the transfer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Start the transfer\n",
    "ol.radio_transmitter.mode = 'repeat'\n",
    "ol.radio_transmitter.start()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The received image above should now contain an image and it should rotate every few seconds. You can use the radio dashboard to change the modulation scheme used by the radio system if desired. The code cell below will stop the demonstration."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.radio_transmitter.stop()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Conclusion\n",
    "This notebook has presented a simple BPSK and QPSK radio demonstrator on the RFSoC using PYNQ. It was shown that data can be transmitted and received correctly using BPSK and QPSK modulation and the RF DCs.\n",
    "\n",
    "The next set of notebooks introduce the RFSoC's Soft-Decision Forward Error Correction (SD-FEC) block."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "[⬅️ Previous Notebook](02_rfsoc_radio_observe.ipynb) || [Next Notebook 🚀](../notebook_H/01_fec_first_principles.ipynb)\n",
    "\n",
    "Copyright © 2023 Strathclyde Academic Media\n",
    "\n",
    "---\n",
    "---"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
